#import CERTMT_sched as cert
#import npCERTMT_sched as np
#import baseline_sched as base
#import MultiFrameNoSMT_sched as multi
#import OneFrameSizeSMT_sched as oneFrame
#import fullPreemption2_sched as fullPreempt
from random import *
import numpy as np
from gurobipy import *
import itertools
import RTCSA_ILP as ILP

FAKE_ZERO = 0.00000000001
UMA_RESULT=0
FUNK_RESULT=1

# DISSERTATION new constants
FIXED = 0
EXPO = 1

BASELINE=1
#Preemptive CE scheduler
CERT_MT=2
#multiple frames, non-SMT tasks are preemptable
FAIL=3
RUNTIME = 3

NUM_PARTITION_ALGS=4
WORST_FIT=0
BEST_FIT=1
PERIOD_AWARE_WORST=2
PERIOD_AWARE_BEST=3

NUM_INNER_COST_MODELS=5
NO_PREEMPTION=0
DOUBLE_COST=1
DATA_DRIVEN=2
PREEMPTION_POINT=3
FULL_PREEMPTION=4

class TaskSet:
    """Description String"""

    def __init__(self, setUtil, utilMin, utilMax, expected_score, slope, prob_harm, degree_harm, score_distro, core_count,
                 periodMin, periodCount, timeout, solutionLimit, lowerBound, upperBound, threadsPerTest):
        """Creates taskSet made of SmartTasks"""
        self.setUtil = setUtil
        self.utilMin = utilMin
        self.utilMax = utilMax
        self.expected_score = expected_score    #\ mu in dissertation
        self.prob_harm = prob_harm              # h in paper
        self.degree_harm = degree_harm          # r in paper
        self.score_distro = score_distro        # exponential or fixed
        self.m = core_count
        self.timeout = timeout
        self.threadsPerTest = threadsPerTest
        self.solutionLimit = solutionLimit
        self.lowerBound = lowerBound
        self.upperBound = upperBound
        self.periodMin = periodMin
        self.nTotal = 0
        self.slope = slope
        if prob_harm <=0 or prob_harm >= 1:
            # making everything harmful has same effect as making nothing harmful.
            self.harm_multiplier = 1
            self.standard_multiplier = 1
        else:
            self.standard_multiplier = 1 /(prob_harm * (degree_harm-1) +1 )
            self.harm_multiplier = self.standard_multiplier * degree_harm
        self.allTasks = []
        # DISSERTATION what does this do?

        # create periods
        self.hyperperiod = periodMin * 2 ** (periodCount - 1)
        self.periods = []

        # print("Creating periods")
        for i in range(periodCount):
            self.periods.append(periodMin * (2 ** i))
            # print(self.periods[i])

        # create tasks, but don't set s_{i:j} values yet
        self.totalUtil = 0
        permID = 0
        while self.totalUtil < setUtil:
            # create task
            util = random() * (utilMax - utilMin) + utilMin
            # DISSERTATION edits
            # these should be random functions
            baseline_intercept = weakness = np.random.exponential(expected_score)
            if random() <= prob_harm:
                harm = 1
            else:
                harm = 0
            period = choice(self.periods)
            self.allTasks.append(SmartTask(util, weakness, harm, period, self.slope, baseline_intercept, permID))
            self.totalUtil = self.totalUtil + util
            permID = permID + 1
        self.nTotal = permID

        # assign sym values
        for i in range(0, self.nTotal):
            task_i = self.allTasks[i]
            #task.symRaw = []
            #task.symAdj = []
            for j in range(0, self.nTotal):
                task_j = self.allTasks[j]
                # DISSERTATION
                if i==j:
                    task_i.symRaw.append(0)
                    task_i.symAdj.append(0)
                    task_i.allCosts.append(task_i.cost)
                else:
                    expected_score_pair = task_i.baseline_intercept + task_i.slope \
                    * (max(0, ((task_i.cost / task_j.cost)-1)))
                    if self.score_distro == FIXED:
                        task_i.symRaw.append(expected_score_pair)
                    elif self.score_distro == EXPO:
                        task_i.symRaw.append(np.random.exponential(expected_score_pair))
                    #print("task i " + str(i))
                    #print("task j " + str(j))
                    #print("score distro: " + str(self.score_distro))
                    #print("task i symRaw: " + str(task_i.symRaw))

                    # don't allow threading when costs differ by factor of 10+
                    ratio = task_i.cost / task_j.cost
                    if ratio> 10 or ratio < 0.1:
                        sym_adj = 10
                    else:
                        sym_adj = max(task_i.symRaw[j], FAKE_ZERO)
                    thread_cost_i = task_i.cost + sym_adj * min(task_i.cost, task_j.cost)
                    task_i.symAdj.append(sym_adj)
                    task_i.allCosts.append(thread_cost_i)


            #print("Printing task " + str(i))
            #print(task_i)
            #print()

        # assign inner costs
        # have to do this after all tasks have their full complement of costs
        for i in range(0, self.nTotal):
            task_i = self.allTasks[i]
            for j in range(0, self.nTotal):
                task_j = self.allTasks[j]
                if i == j:
                    #solo tasks are fully preemptable
                    for model in range(NUM_INNER_COST_MODELS):
                        task_i.innerCosts[model].append(0)
                else:
                    outer_cost = max(task_i.allCosts[j], task_j.allCosts[i])
                    #max_cost = max(task_i.cost, task_j.cost)
                    min_cost = min(task_i.cost, task_j.cost)

                    task_i.innerCosts[NO_PREEMPTION].append(outer_cost)
                    task_i.innerCosts[DOUBLE_COST].append(min(outer_cost, min_cost*2))
                    task_i.innerCosts[DATA_DRIVEN].append(min_cost)
                    task_i.innerCosts[PREEMPTION_POINT].append(min(min_cost, 10))
                    task_i.innerCosts[FULL_PREEMPTION].append(0)



    def addTask(self):
        #self.nTotal=permID=len(self.allTasks)
        permID=len(self.allTasks)
        util = random() * (self.utilMax - self.utilMin) + self.utilMin
        # DISSERTATION
        baseline_intercept = weakness = np.random.exponential(self.expected_score)
        if random() <= self.prob_harm:
            harm = 1
        else:
            harm = 0
        # actually create the new task
        period = choice(self.periods)
        self.allTasks.append(SmartTask(util, weakness, harm, period, self.slope, baseline_intercept, permID))
        self.totalUtil = self.totalUtil + util
        #set sym values for the just-created task
        task_new = self.allTasks[permID]
        self.nTotal += 1
        # give task_new a cost for every already added task
        # give the old task a cost to go with the new task
        # self.nTotal = task_new.permID + 1, so task_new will be the last task in this loop
        for j in range(0, self.nTotal):
            task_old = self.allTasks[j]
            # DISSERTATION
            if task_new.permID == j:
                task_new.symRaw.append(0)
                task_new.symAdj.append(0)
                task_new.allCosts.append(task_new.cost)

                #update inner costs
                # solo tasks are fully preemptable
                for model in range(NUM_INNER_COST_MODELS):
                    task_new.innerCosts[model].append(0)

            else:
                expected_score_new = task_new.baseline_intercept + task_new.slope \
                                      * max(0, (task_new.cost / task_old.cost - 1))
                expected_score_old = task_old.baseline_intercept + task_old.slope \
                                    * max(0, (task_old.cost /task_new.cost -1))
                if self.score_distro == FIXED:
                    task_new.symRaw.append(expected_score_new)
                    task_old.symRaw.append(expected_score_old)
                elif self.score_distro == EXPO:
                    task_new.symRaw.append(np.random.exponential(expected_score_new))
                    task_old.symRaw.append(np.random.exponential(expected_score_old))
                # don't allow threading when costs differ by factor of 10+
                ratio = task_new.cost / task_old.cost
                if ratio > 10 or ratio < 0.1:
                    sym_adj_new = 10
                    sym_adj_old = 10
                else:
                    sym_adj_new = max(task_new.symRaw[j], FAKE_ZERO)
                    sym_adj_old = max(task_old.symRaw[-1], FAKE_ZERO)
                thread_cost_new = task_new.cost + sym_adj_new * min(task_new.cost, task_old.cost)
                thread_cost_old = task_old.cost + sym_adj_old * min(task_new.cost, task_old.cost)
                task_new.symAdj.append(sym_adj_new)
                task_new.allCosts.append(thread_cost_new)
                task_old.symAdj.append(sym_adj_old)
                task_old.allCosts.append(thread_cost_old)

                # give both tasks new inner costs
                outer_cost = max(thread_cost_new, thread_cost_old)
                # max_cost = max(task_i.cost, task_j.cost)
                min_cost = min(thread_cost_new, thread_cost_old)

                task_new.innerCosts[NO_PREEMPTION].append(outer_cost)
                task_new.innerCosts[DOUBLE_COST].append(min(outer_cost, min_cost * 2))
                task_new.innerCosts[DATA_DRIVEN].append(min_cost)
                task_new.innerCosts[PREEMPTION_POINT].append(min(min_cost, 10))
                task_new.innerCosts[FULL_PREEMPTION].append(0)

                task_old.innerCosts[NO_PREEMPTION].append(outer_cost)
                task_old.innerCosts[DOUBLE_COST].append(min(outer_cost, min_cost * 2))
                task_old.innerCosts[DATA_DRIVEN].append(min_cost)
                task_old.innerCosts[PREEMPTION_POINT].append(min(min_cost, 10))
                task_old.innerCosts[FULL_PREEMPTION].append(0)


    def makePairs(self):
        (thePairs, runtime) = ILP.RTCSA_ILP(self).makePairs()
        return (thePairs, runtime)

    def testSystem(self):

        # test the baseline
        # success iff bin-packing is successful
        successBaseline = self.baselineFit()
        # run the ILP
        (thePairs, runtime) = self.makePairs()
        success_smt = []
        for alg in range(WORST_FIT, PERIOD_AWARE_BEST + 1):
            # until tested, assume this algorithm fails for all inner cost models
            success_smt.append([False, False, False, False, False])
            # try for just NO_PREEMPTION and FULL_PREEMPTION
            # success_smt.append([False, False])
            assignments = self.partitionAlgorithm(thePairs, alg)
            if assignments == False:
                # bin-packing failed
                # move on to next bin-packing algorithm
                continue
            else:
                # testing just NO_PREEMPTION and FULL_PREEMPTION
                # success_smt[alg][0]=self.testPartition(assignments, NO_PREEMPTION)
                # success_smt[alg][1]=True

                success_smt[alg][NO_PREEMPTION] = self.testPartition(assignments, NO_PREEMPTION)
                success_smt[alg][DOUBLE_COST] = self.testPartition(assignments, DOUBLE_COST)
                success_smt[alg][DATA_DRIVEN] = self.testPartition(assignments, DATA_DRIVEN)
                success_smt[alg][PREEMPTION_POINT] = self.testPartition(assignments, PREEMPTION_POINT)
                success_smt[alg][FULL_PREEMPTION] = True
                '''
                for model in range(NUM_INNER_COST_MODELS):
                    #print("Testing alg. ", alg, "model ", model)
                    if model==FULL_PREEMPTION:
                        success_smt[alg][model]=True
                        continue
                    else:
                        #check if bin-packing created schedulable cores
                        #after accounting for blocking
                        #print("Testing alg. ", alg, "model ", model)
                        success_smt[alg][model]=self.testPartition(assignments, model)
                '''
        return (successBaseline, success_smt), runtime

    def testPartition(self, assignments, innerCostModel):
        # we don't need to test total utilization
        # this function runs only if we're ok on total util for all cores

        '''
        partitionList is a list of m lists, each corresponding to a single core.
        Each core list contains a number of tuples, with each tuple corresponding to a given task pair.
        '''
        for coreList in assignments:
            totalUtil = 0
            # print ("coreList:", coreList)
            # for taskPair in coreList:
            for i in range(len(coreList)):
                taskPair = coreList[i]
                task1_id = taskPair[0]
                task2_id = taskPair[1]
                #DISSERTATION this needs to be the max of the two costs
                #totalUtil = totalUtil + self.allTasks[task1].allCosts[task2] / self.allTasks[task1].period
                max_cost = max(self.allTasks[task1_id].allCosts[task2_id], self.allTasks[task2_id].allCosts[task2_id])
                totalUtil = totalUtil + max_cost / self.allTasks[task1_id].period

            # for blockedTask in coreList:
            for i in range(len(coreList)):
                blockedTask = coreList[i]
                task1_id = blockedTask[0]
                task2_id = blockedTask[1]
                blockedPeriod = self.allTasks[task1_id].period
                maxBlock = 0

                # for blockingTask in coreList:
                for j in range(len(coreList)):
                    blockingTask = coreList[j]
                    block1 = blockingTask[0]
                    block2 = blockingTask[1]
                    blockingPeriod = self.allTasks[block1].period
                    if blockingPeriod > blockedPeriod:
                        blockTime = self.allTasks[block1].innerCosts[innerCostModel][block2]
                        maxBlock = max(maxBlock, blockTime)
                # this line is never executed!
                if totalUtil + maxBlock / blockedPeriod > 1:
                    return False
        return True

    def testUtil(self, newCoreUtil, utilOnBest, useWorstFit):
        # test for worst-fit heuristic
        if useWorstFit:
            return newCoreUtil <= utilOnBest
        # else test for best-fit heuristic
        else:
            return newCoreUtil >= utilOnBest

    def baselineFit(self):
        # applies decreasing worst-fit bin-packing to tasks
        # no SMT used
        coreCount = self.m
        utilOnCore = [0] * coreCount
        # tasksByCore=[0]*coreCount
        sortedTasks = sorted(self.allTasks, key=lambda task: task.util, reverse=True)

        for task in sortedTasks:
            bestCoreSoFar = -1
            utilOnBest = 1

            for c in range(len(utilOnCore)):
                newCoreUtil = utilOnCore[c] + task.util
                if newCoreUtil < 1 and newCoreUtil >= bestCoreSoFar:
                    bestCoreSoFar = c
                    utilOnBest = newCoreUtil
            if bestCoreSoFar == -1:
                return False
            else:
                utilOnCore[bestCoreSoFar] = utilOnBest
                # don't need to keep track of which tasks are on which core
                # just need to know all tasks are on a core
        return True

    def partitionAlgorithm(self, thePairs, alg):
        if alg == WORST_FIT:
            result = self.pureFit(thePairs, useWorstFit=True)
        elif alg == BEST_FIT:
            result = self.pureFit(thePairs, useWorstFit=False)
        elif alg == PERIOD_AWARE_WORST:
            result = self.twoPassFit(thePairs, useWorstFit=True)
        elif alg == PERIOD_AWARE_BEST:
            result = self.twoPassFit(thePairs, useWorstFit=False)
        return result

    def pureFit(self, thePairs, useWorstFit):
        # using 0-indexed cores
        coreCount = self.m
        utilOnCore = [0] * coreCount
        # pairsByCore=[]*coreCount
        # pairsByCore=[0]*coreCount
        pairsByCore = []
        for c in range(coreCount):
            pairsByCore.append([])
        # sort pairs by threadedUtil
        sortedPairs = sorted(thePairs, key=lambda x: x[ILP.UTIL], reverse=True)

        for p in sortedPairs:
            # find a core we can use

            bestCoreSoFar = -1
            if useWorstFit:
                utilOnBest = 1
            else:
                utilOnBest = 0

            for c in range(len(utilOnCore)):
                newCoreUtil = utilOnCore[c] + p[ILP.UTIL]
                # if newCoreUtil<=1 and newCoreUtil<=utilOnBest:
                if newCoreUtil <= 1 and self.testUtil(newCoreUtil, utilOnBest, useWorstFit):
                    bestCoreSoFar = c
                    utilOnBest = newCoreUtil
            # done looping through cores

            # add pair to core and update core contents
            if bestCoreSoFar == -1:
                # print()
                # print("Worst-fit bin-packing failed.")
                # print()
                return False
            else:
                utilOnCore[bestCoreSoFar] = utilOnBest
                # has to be a better way to do this
                # if pairsByCore[bestCoreSoFar]==0:
                # pairsByCore[bestCoreSoFar]=[]
                pairsByCore[bestCoreSoFar].append(p)
        # returns only if all pairs could be placed on a core
        # for c in range(coreCount):
        # print(pairsByCore[c])
        # print (pairsByCore)
        return pairsByCore

    def twoPassFit(self, thePairs, useWorstFit):
        coreCount = self.m
        utilOnCore = [0] * coreCount
        # pairsByCore=[0]*coreCount
        pairsByCore = []
        for c in range(coreCount):
            pairsByCore.append([])
        # periodsOnCore=[[0]]*coreCount
        multiplePeriodsOnCore = [False] * coreCount
        firstPeriodOnCore = [0] * coreCount

        # sort pairs by threadedUtil
        sortedPairs = sorted(thePairs, key=lambda x: x[ILP.UTIL], reverse=True)

        for pair in sortedPairs:
            bestCoreSoFar = -1
            if useWorstFit:
                utilOnBest = 1
            else:
                utilOnBest = 0

            # first pass: consider only cores without conflicting periods
            for c in range(len(utilOnCore)):
                newCoreUtil = utilOnCore[c] + pair[ILP.UTIL]
                if (newCoreUtil <= 1 and self.testUtil(newCoreUtil, utilOnBest, useWorstFit)):
                    if multiplePeriodsOnCore[c] == False:
                        if (firstPeriodOnCore[c] == 0 or firstPeriodOnCore[c] == pair[ILP.PERIOD]):
                            bestCoreSoFar = c
                            utilOnBest = newCoreUtil
            # done looping through cores

            # add pair to core and update core contents
            if bestCoreSoFar != -1:
                utilOnCore[bestCoreSoFar] = utilOnBest
                # has to be a better way to do this
                # if pairsByCore[bestCoreSoFar]==0:
                # pairsByCore[bestCoreSoFar]=[]
                pairsByCore[bestCoreSoFar].append(pair)
                firstPeriodOnCore[bestCoreSoFar] = pair[ILP.PERIOD]


            # second pass: consider all cores
            else:
                bestCoreSoFar = -1
                if useWorstFit:
                    utilOnBest = 1
                else:
                    utilOnBest = 0
                for c in range(len(utilOnCore)):
                    newCoreUtil = utilOnCore[c] + pair[ILP.UTIL]
                    if (newCoreUtil <= 1 and self.testUtil(newCoreUtil, utilOnBest, useWorstFit)):
                        bestCoreSoFar = c
                        utilOnBest = newCoreUtil
                # done looping through cores

                if bestCoreSoFar == -1:
                    # print()
                    # print("Two pass worst-fit bin-packing failed.")
                    # print()
                    return False
                else:
                    utilOnCore[bestCoreSoFar] = utilOnBest
                    if pairsByCore[bestCoreSoFar] == 0:
                        pairsByCore[bestCoreSoFar] = []
                    pairsByCore[bestCoreSoFar].append(pair)
                    # technically this over
                    multiplePeriodsOnCore[bestCoreSoFar] = True
        '''
        for c in range(coreCount):
            print(pairsByCore[c])
        '''
        return pairsByCore

    def printTaskSystem(self, model):
        print("totalUtil: ", self.totalUtil)
        for i in range(self.nTotal):
            task = self.allTasks[i]
            print(i, "period: ", task.period, "cost: ", task.cost, "costModel: ", model,
                  "|\n otherCosts: ", task.allCosts,
                  "\n innerCosts: ", task.innerCosts[model])
            print()


class SmartTask:
    """description string"""
    #harm: int

    def __init__(self, util, weakness, harm, period, slope, baseline_intercept, permID):
        self.util = float(util)
        self. weakness = weakness
        self.harm = harm
        self.symRaw = []  # will hold s_{i:j} values for all j; may be <0
        self.symAdj = []  # all values >= 0
        self.period = period
        self.cost = period * util
        self.permID = permID
        self.allCosts = []
        self.slope = slope
        self.baseline_intercept = baseline_intercept
        #five lists corresponding to five innerCost models
        self.innerCosts=([], [], [], [], [])

    def __str__(self):
        strSymAdj = str(self.symAdj)
        strAllCosts = str(self.allCosts)
        #return str(str(self.permID) + " " + str(self.util) + " " + strSymAdj)
        return str(str(self.permID) + " w=" + str(self.weakness) + " h=" + str(self.harm) + "\n" +
                   "u=" + str(self.util) + " T=" + str(self.period) + " C=" + str(self.cost) + "\n" +
                   "Scores: \n" + strSymAdj + "\n" + "Costs: \n" +
                   strAllCosts)

        # every task needs a unique ID that holds across all partitions


#def __init__(self, setUtil, utilMin, utilMax, expected_score, slope, prob_harm, degree_harm, score_distro, core_count,
#                 periodMin, periodCount, timeout, solutionLimit, lowerBound, upperBound, threadsPerTest):
myTaskSet = TaskSet(2, 0.1, 0.5, .55, 0.0, 0, 2, FIXED, 2,
                    10, 1, 30, 1000, 1, 1, 0)

#myTaskSet.printTaskSystem(model=NO_PREEMPTION)
#print("No SMT, Results per Bin-pack method.")
#print(myTaskSet.testSystem())

thePairs=ILP.RTCSA_ILP(myTaskSet).makePairs()[0]
#print("Printing thePairs:")
#print(thePairs)

print("Printing thePairs on at a time: ")
for pair in thePairs:
    print(pair)
    print()

print("Attempting pure worst fit.")
myTaskSet.pureFit(thePairs, useWorstFit=True)

print("")
print("Attempting two pass worst fit.")
myTaskSet.twoPassFit(thePairs, useWorstFit=True)

print()
print("Attempting pure best fit.")
myTaskSet.pureFit(thePairs, useWorstFit=False)

print("")
print("Attempting two pass best fit.")
myTaskSet.twoPassFit(thePairs, useWorstFit=False)

#myTaskSet.periodGroupsByUtil(thePairs)

print("Done with RTCSA_TaskSystem main.")


#for task in myTaskSet.allTasks:
#    print(task)
#    print("")


